/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server.plugin.impl;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;

import jp.fores.midori.core.dto.RPCInputDto;
import jp.fores.midori.core.dto.RPCOutputDto;
import jp.fores.midori.server.WebContext;
import jp.fores.midori.server.exception.MidoriServerException;
import jp.fores.midori.server.exception.RPCIllegalArgumentException;
import jp.fores.midori.server.exception.RPCIllegalRequestException;
import jp.fores.midori.server.exception.RPCTargetMethodNotFoundException;
import jp.fores.midori.server.exception.RPCTargetServiceNotFoundException;
import jp.fores.midori.server.plugin.AbstractRPCPlugin;
import net.arnx.jsonic.JSON;
import net.arnx.jsonic.JSONException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * JSON-RPC形式のリクエスト・レスポンスの処理を行うプラグインクラス。<br>
 * <br>
 * 通常のJSON-RPC形式に加えて、JSONPにも対応しています。<br>
 * パラメーターに「callback」が含まれている場合は、JSONP用の動作をします。<br>
 * JSONPの場合も、リクエスト・レスポンスの内容自体は通常のJSON-RPCの場合と同じとして扱います。<br>
 * JSONPの場合はmethod, id, paramsの値は、dataという名前のパラメーターにJSON形式の文字列にひとまとめにして送るようにして下さい。<br>
 */
public class JsonRPCPlugin extends AbstractRPCPlugin {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(JsonRPCPlugin.class);

	/**
	 * Content-Type(JSON用)
	 */
	public static final String CONTENT_TYPE_JSON = "application/json";

	/**
	 * Content-Type(JSONP用)
	 */
	public static final String CONTENT_TYPE_JSONP = "application/javascript; charset=UTF-8";


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//エラーメッセージ関連
	//(JSON-RPCの仕様で定義されている)

	/**
	 * パースエラーの場合のエラーコード
	 */
	public static final int ERROR_CODE_PARSE_ERROR = -32700;

	/**
	 * 不正なリクエストの場合のエラーコード
	 */
	public static final int ERROR_CODE_INVALID_REQUEST = -32600;

	/**
	 * メソッドが見つからなかった場合のエラーコード
	 */
	public static final int ERROR_CODE_METHOD_NOT_FOUND = -32601;

	/**
	 * 不正な引数の場合のエラーコード
	 */
	public static final int ERROR_CODE_INVALID_PARAMS = -32602;

	/**
	 * 内部エラーが発生した場合のエラーコード
	 */
	public static final int ERROR_CODE_INTERNAL_ERROR = -32603;

	/**
	 * デフォルトのエラーメッセージ
	 */
	public static final String DEFAULT_ERROR_MESSAGE = "Server error.";

	/**
	 * エラーコードとエラーメッセージの対応を保持するMap
	 */
	protected static final Map<Integer, String> errorMessageMap = new HashMap<Integer, String>();


	//==========================================================
	//staticイニシャライザ
	static {
		//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
		//エラーコードとエラーメッセージの対応を保持するMapの初期化
		errorMessageMap.put(ERROR_CODE_PARSE_ERROR, "Parse error.");
		errorMessageMap.put(ERROR_CODE_INVALID_REQUEST, "Invalid Request.");
		errorMessageMap.put(ERROR_CODE_METHOD_NOT_FOUND, "Method not found.");
		errorMessageMap.put(ERROR_CODE_INVALID_PARAMS, "Invalid params.");
		errorMessageMap.put(ERROR_CODE_INTERNAL_ERROR, "Internal error.");
	}


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public RPCInputDto requestToInputDto(WebContext context)
			throws RPCIllegalRequestException, Exception {
		//==========================================================
		//下準備

		//JSONICのJSONクラスのインスタンスを生成
		JSON json = new JSON();

		//JSONクラスのロケールにリクエストのロケールを設定する
		json.setLocale(context.request.getLocale());


		//==========================================================
		//リクエストのcallbackパラメーターを取得
		//(これが指定されているとJSONPとみなす)
		String callback = context.request.getParameter("callback");

		//callbackをリクエスト属性として登録しておく
		//(レスポンスを出力する時にこの値を元にして、JSONPかどうか判断する必要があるため)
		context.request.setAttribute("callback", callback);


		//==========================================================
		//リクエストの内容をJSON-RPC形式のMapに変換
		Map<String, Object> map = null;

		//callbackが取得できた場合
		//(JSONP用にする場合)
		if (callback == null) {
			//JSONクラスを使って、リクエストのリーダーの内容(JSON形式の文字列)をMapに変換する
			map = (Map) json.parse(context.request.getReader());
		}
		//それ以外の場合
		else {
			//リクエストのdataパラメーターを取得
			//(ここにJSON-RPC用のデータがJSON形式の文字列でひとまとめにして含まれている手はずになっている)
			String data = context.request.getParameter("data");

			//dataパラメーターが取得できた場合
			if (data != null) {
				//JSONクラスを使って、dataパラメーターの内容(JSON形式の文字列)をMapに変換する
				map = (Map) json.parse(data);
			}
		}

		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("JSON形式のリクエストの内容のMap:" + map);
		}


		//==========================================================
		//JSON-RPC形式のMapの内容をRPCの入力データを格納するためのDTOに変換する

		//Map自体が取得できなかった場合、またはメソッド名や引数の情報が取得できなかった場合
		if ((map == null) || (map.get("method") == null)
				|| (map.get("params") == null)) {
			//RPCのリクエストが不正な場合の例外を投げる
			throw new RPCIllegalRequestException("リクエストに必要な情報が含まれていませんでした。");
		}

		//RPCの入力データを格納するためのDTOクラスのインスタンスを生成
		RPCInputDto inputDto = new RPCInputDto();

		//リクエスト内容から作成したMapからメソッド名と引数の情報を取得して、DTOに設定する
		inputDto.methodName = (String) map.get("method");
		inputDto.args = ((List<Object>) map.get("params")).toArray(new Object[0]);

		//リクエスト内容から作成したMapからidを取得して、リクエスト属性として登録しておく
		//(レスポンスを出力する時にこの値を含める必要があるため)
		context.request.setAttribute("id", map.get("id"));

		//作成したDTOを返す
		return inputDto;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void outputDtoToResponse(RPCOutputDto outputDto, WebContext context)
			throws Exception {
		//==========================================================
		//下準備

		//JSONICのJSONクラスのインスタンスを生成
		JSON json = new JSON();

		//JSONクラスのロケールにリクエストのロケールを設定する
		json.setLocale(context.request.getLocale());

		//リクエスト属性から、リクエストの処理のときにあらかじめ設定しておいたcallbackを取得
		String callback = (String) context.request.getAttribute("callback");

		//callbackが取得できた場合
		//(JSONP用にする場合)
		if (callback != null) {
			//レスポンスのContent-TypeにJSONP用の値を設定する
			context.response.setContentType(CONTENT_TYPE_JSONP);
		}
		//それ以外の場合
		else {
			//レスポンスのContent-TypeにJSON用の値を設定する
			context.response.setContentType(CONTENT_TYPE_JSON);
		}

		//リクエスト属性から、リクエストの処理のときにあらかじめ設定しておいたidを取得
		//(idの型は数値だったり文字列だったりするので、Object型で受けておく)
		Object id = context.request.getAttribute("id");

		//idが取得できなかった場合
		if (id == null) {
			//レスポンスにリクエストが受け付けられたが処理が完了しなかったことを示すステータスコードを設定する
			//(元ネタにあった処理なので、そのまま移植しておく)
			context.response.setStatus(HttpServletResponse.SC_ACCEPTED);
		}


		//==========================================================
		//RPCの出力データを格納するためのDTOをJSON-RPCのレスポンス形式にあわせたMapに変換

		//レスポンス形式にあわせたMapのインスタンスを生成
		//(出力する順番も意識する必要があるため、HashMapではなくLinkedHashMapを使う必要がある)
		Map<String, Object> res = new LinkedHashMap<String, Object>();

		//戻り値のコードが「OK」の場合
		//(サービス呼び出しに成功した場合)
		if (outputDto.returnCode == RPCOutputDto.RETURN_CODE_OK) {
			//レスポンス形式にあわせたMapの「result」に、出力データを格納するDTOに設定されているオブジェクトを設定
			res.put("result", outputDto.data);

			//レスポンス形式にあわせたMapの「error」に、nullを設定
			res.put("error", null);
		}
		//それ以外の場合
		//(サービス呼び出しに失敗した場合)
		else {
			//レスポンス形式にあわせたMapの「result」に、nullを設定
			res.put("result", null);


			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//JSON-RPCのエラー情報を格納するためのMapにエラー内容を詰める

			//戻り値のコードをエラーコードとして使用する
			int errorCode = outputDto.returnCode;

			//JSON-RPCのエラー情報を格納するためのMapを生成
			Map<String, Object> error = new LinkedHashMap<String, Object>();

			//エラー情報を格納するためのMapの「code」に、エラーコードを設定
			error.put("code", errorCode);

			//エラー情報を格納するためのMapの「message」に、エラーコードに対応するエラーメッセージを設定
			error.put("message", getErrorMessage(errorCode));

			//エラー情報を格納するためのMapの「data」に、出力データを格納するDTOに設定されているオブジェクト(例外オブジェクト)を設定
			error.put("data", outputDto.data);

			//レスポンス形式にあわせたMapの「error」に、エラー情報を格納するためのMapを設定
			//(入れ子構造にする)
			res.put("error", error);
		}

		//レスポンス形式にあわせたMapの「id」に、取得したidを設定
		//(取得できなかった場合はnullを設定する)
		res.put("id", id);


		//==========================================================
		//作成したJSON-RPCのレスポンス形式にあわせたMapを元にして、レスポンスに出力

		try {
			//JSONのコンテキストとして、出力データとして設定されているオブジェクトを設定
			//(JavaオブジェクトをJSON形式に変換するためのヒント情報を与えるため)
			//(エラーの場合にはこの値はnullになっているが、nullを設定しても問題はない)
			json.setContext(res.get("result"));

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//JSONの出力形式を可読性が高い形式にする
				json.setPrettyPrint(true);
			}

			//レスポンスのWriterを取得
			//(念のためBufferedWriterでラッピングする)
			Writer writer = new BufferedWriter(context.response.getWriter());

			try {
				//callbackが取得できた場合
				//(JSONP用にする場合)
				if (callback != null) {
					//コールバック関数の呼び出し形式にするための文字列をWriterに出力する
					writer.write(callback);
					writer.write("(");
				}

				//JSONクラスを使って、作成したMapをレスポンスのWriterに出力する
				json.format(res, writer);

				//callbackが取得できた場合
				//(JSONP用にする場合)
				if (callback != null) {
					//コールバック関数の呼び出し形式にするための文字列をWriterに出力する
					writer.write(")");
				}
			}
			//終了処理
			finally {
				//レスポンスのWriterを確実に閉じる
				IOUtils.closeQuietly(writer);
			}
		}
		//入出力例外が発生した場合
		catch (IOException e) {
			//どうしようもないので、捕捉した例外をそのまま投げる
			throw e;
		}
		//その他の例外が発生した場合
		catch (Exception e) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("レスポンスの出力時に例外が発生しました。", e);
			}

			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//どうしようもないので、内部エラーが発生した場合の内容でレスポンス用のMapを作成しなおす
			//(上にあるエラー処理と重複するのが実装的にいまいち・・・)
			int errorCode = ERROR_CODE_INTERNAL_ERROR;
			res = new LinkedHashMap<String, Object>();
			res.put("result", null);
			Map<String, Object> error = new LinkedHashMap<String, Object>();
			error.put("code", errorCode);
			error.put("message", getErrorMessage(errorCode));
			error.put("data", e);
			res.put("error", error);
			res.put("id", id);

			//レスポンスのWriterを取得
			//(念のためBufferedWriterでラッピングする)
			Writer writer = new BufferedWriter(context.response.getWriter());

			try {
				//callbackが取得できた場合
				//(JSONP用にする場合)
				if (callback != null) {
					//コールバック関数の呼び出し形式にするための文字列をWriterに出力する
					writer.write(callback);
					writer.write("(");
				}

				//JSONクラスを使って、作成したMapをレスポンスのWriterに出力する
				json.format(res, writer);

				//callbackが取得できた場合
				//(JSONP用にする場合)
				if (callback != null) {
					//コールバック関数の呼び出し形式にするための文字列をWriterに出力する
					writer.write(")");
				}
			}
			//終了処理
			finally {
				//レスポンスのWriterを確実に閉じる
				IOUtils.closeQuietly(writer);
			}
		}
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//オーバーライドするメソッド

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void handleError(Throwable e, RPCOutputDto outputDto) {
		//==========================================================
		//例外の種類に応じて適切なエラーコードを設定する

		//エラーコード
		//(デフォルト値は内部エラーが発生した場合のエラーコード)
		int errorCode = ERROR_CODE_INTERNAL_ERROR;

		//捕捉した例外が動的に呼び出したメソッドで発生する例外の場合
		if (e instanceof InvocationTargetException) {
			//対象の例外を捕捉した例外の発生元に切り替える
			//(InvocationTargetException自体には価値がないため)
			e = e.getCause();
		}


		//捕捉した例外がRPCのリクエストが不正な場合に発生する例外の場合
		if (e instanceof RPCIllegalRequestException) {
			//不正なリクエストの場合のエラーコードを設定する
			errorCode = ERROR_CODE_INVALID_REQUEST;
		}
		//捕捉した例外がRPCの対象のサービスクラスが見つからなかった場合に発生する例外の場合、
		//またはRPCの対象のメソッドが見つからなかった場合に発生する例外の場合
		else if ((e instanceof RPCTargetServiceNotFoundException)
				|| (e instanceof RPCTargetMethodNotFoundException)) {
			//メソッドが見つからなかった場合のエラーコードを設定する
			errorCode = ERROR_CODE_METHOD_NOT_FOUND;
		}
		//捕捉した例外がRPCの引数の形式が不正な場合に発生する例外の場合
		else if (e instanceof RPCIllegalArgumentException) {
			//不正な引数の場合のエラーコードを設定する
			errorCode = ERROR_CODE_INVALID_PARAMS;
		}
		//捕捉した例外がJSON関連の例外の場合
		else if (e instanceof JSONException) {
			//パース後に発生したエラーの場合
			if (((JSONException) e).getErrorCode() == JSONException.POSTPARSE_ERROR) {
				//不正な引数の場合のエラーコードを設定する
				errorCode = ERROR_CODE_INVALID_PARAMS;
			}
			//それ以外の場合
			else {
				//パースエラーの場合のエラーコードを設定する
				errorCode = ERROR_CODE_PARSE_ERROR;
			}
		}

		//出力データ用のDTOの戻り値のコードにエラーコードを設定する
		outputDto.returnCode = errorCode;
		//==========================================================


		//捕捉した例外がmidoriのサーバー側で発生する例外の場合
		if (e instanceof MidoriServerException) {
			//捕捉した例外をそのまま出力データに設定
			outputDto.data = e;
		}
		//それ以外の場合
		else {
			//捕捉した例外のメッセージをリモートプロシージャーコール用のサーブレットで発生する例外でラッピングし、出力データに設定
			//(捕捉した例外のスタックトレースを含んでしまうと、クライアント側に存在しない例外クラスの場合に、
			// デシリアライズに失敗してしまうため、メッセージのみを引き継ぐ)
			outputDto.data = new MidoriServerException(e.getMessage());
		}

	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * エラーコードに対応するエラーメッセージを取得します。<br>
	 *
	 * @param errorCode エラーコード
	 * @return エラーメッセージ
	 */
	protected String getErrorMessage(int errorCode) {
		//エラーコードとエラーメッセージの対応を保持するMapから、エラーコードに対応するエラーメッセージを取得
		String errorMessage = errorMessageMap.get(errorCode);

		//エラーメッセージが取得できなかった場合
		if (errorMessage == null) {
			//デフォルトのエラーメッセージを代わりに使用する
			errorMessage = DEFAULT_ERROR_MESSAGE;
		}

		//取得したエラーメッセージを返す
		return errorMessage;
	}

}
